有些問題需要在其他不相關的物件之間共用行為。這種共同行為對類別來說是正確的 ,它是物件所扮演的角色。
在設計物件導向程式時,有些問題需要多個不相關的物件之間共享相似的行為,這種共同行為被視為角色,物件可以扮演這些角色。
當不相關的物件開始扮演相同的角色時,它們建立了一種依賴關係。這種關係不同於繼承的子類別/父類別關係,需要辨別這些角色,最小化他們之間的依賴關係。
還記得我們在第5章裡提到的鴨子類型嗎?鴨子類型Preparer就是一個角色。只要實作了Preparer介面的物件就扮演了這個角色。Mechanic類別、Tripcoordinator類別和Driver類別實作了prepare_trip方法,可以被視為Preparer,並與其他物件互動。
許多物件導向語言都提供了某種方式,可以定義一組被命名的方法。這些方法獨立於類別,並且可以被物件使用。在 Ruby裡,這種混入內容被稱為 模組 (module) ,然後這個模組可以加入到任何物件,模組裡的方法藉由自動委派能夠被物件使用。
我們可以從物件的角度來看,雖然跟繼承有點相似,但實際上的運作是如果物件接收到無法理解的訊息,那麼這些訊息會自動轉遞到其他地方;接著,正確的方法實作會被神奇地找到,然後執行,並傳回回應。
一個包含模組的物件可回應的所有訊息包括有:
當你遇到有許多相同的程式碼時,可以思考是否要建立鴨子類型,並將共用行為放入模組,而在這之前我們需要確認物件們的行為:
假設存在有一個Schedule類別,其介面已經包含了下面三個方法,每一個方法都帶有三個參數:實際目標,以及特定時間範圍的開始和結束日期。Schedule負責瞭解其傳入的target參數是否已被安排,並負責在排程表裡加入或移除target。
scheduled?(target, starting, ending)add(target, starting, ending)remove(target, starting/ ending)

Schedule自身會負責知道正確的前置時間。schedulable?方法知道所有可能的值,並且它會檢查傳入target參數的類別,來決定該使用哪一種前置時間。這個實作例子問題在於 Schedule都知道太多,這些資訊不該由Schedule提供,而應該屬於Schedule所檢查的類別。
Schedulable鴨子類別
將檢查類別的職責從schedulable?方法裡移除,並且會將lead_days訊息傳送給傳入的target參數。
Schedule類別不在乎target的類別,期望target能夠理解lead_days,也可以說是表現得像
schedulable的事物。
這項修改的目的是簡化程式碼,將責任推給最終物件,而不是對於類別的檢查。

讓物件自己說話
假設有一個StringUtils類別,它實作了管理字串的實用方法。你可以向StringUtils傳送StringUtils.empty?(some_string)訊息,來詢問某個字串是否為空。
物件應自我管理:
圖 7.2 裡的那張順序圖違反了這項規則。發起者試圖確認target物件是否可調度。但它不會向target詢問    這個問題,實際上它問的是第三方(即Schedule),這樣做會迫使發起者知道並依賴於Schedule,這樣做會迫發起者知道並依賴於Schedule。
先選擇一個具體的類別Bicycle,並在該類別裡面直接實作schedulable?方法,在做這項修改之前,每一個發起物件都必須知道Schedule,進而形成一段依賴關係 。

class Schedule
	def scheduled?(schedulable, start_date, end_date) 
		puts "This #{schedulable.class} " +
			"is not scheduled\n" +
			" between #{start_date) and #{end_date}" 
		false
	end
end
Bicycle知道自己的調度前置時間,並且將schedulable?委派給了Schedule。
class Bicycle
	attr_reader :schedule, :size, :chain, :tire_size
	
	# 注入 Schedule,並提供預設值
	def initialize(args = {})
		@schedule = args[:schedule] || Schedule.new 
		# ...
	end
	# 如果這輛自行車在(現在由 Bicycle 指定的)
	# 這段時間內可用就傳回真
	def scheedulable?(start_date, end_date)
		!scheduled?(start_date - lead_days, end_date )
	end
	# 傳回 schedule 的回應
	def scheduled?
		schedule.scheduled?(self, start_date, end_date)
	end
	
	# 傳回自行車可被調度前的
	# lead_days 數值
	def lead_days
		1
	end
	# ...
end
require 'date'
starting = Date.parse("2015/09/04")
ending = Date.parse("2015/09/10")
b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
#   between 2015-09-03 and 2015-09-10 
#  => true
這段程式碼將「Schedule是誰」以及「它做了什麼」的知識隱藏在Bicycle裡面。 持有Bicycle的物件不再需要知道Schedule的存在或者其行為。
Bicycle並不是唯一「可調度」的。Mechanic和Vehicle也都會扮演這個角色,因此它們也需要這個行為,現在需要重新安排這段程式碼,讓它能在不同類別之間共用。
module Schedulable
	attr_writer :schedule
	def schedule
		@schedule ||= ::Schedule.new
	end
	def schedulable?(start_date, end_date) 
		!scheduled?(start_date - lead_days, end_date)
	end
	def scheduled?(start_date, end_date) 
		schedule.scheduled?(seif, start_date, end_date)
	end
	# 包含者可以加以覆蓋
	def lead_days 
		0
	end
end
上面的範例展示了一個新的Schedulable模組。它包含一個從Bicycle擷取出來的抽象。
增加了一個schedule方法,對Schedule的依賴關係已從Bicycle裡移除,並且被移到Schedulable 模組,使其更加受到隔離。
之前Bicycle所實作的版本會傳 回針對自行車旳數值'現在,這個模組的實作版本會傳回一個更為普遍的預設值(即 0 天)。Schedulable模組也必須要實作lead_days方法。針對模組的規則與針對經典繼承的規則是一樣的。如果某個模組想要傳送訊息,它必須提供實作。lead_days方法是一個鉤子,它遵循範本方法模式。Bicycle則覆蓋了這個鉤子(第4行),來提供自己的特殊化。
class Bicycle
	include Schedulable
	def lead_days 
		1
	end
	#...
end
require 'date'
starting = Date.parse("2015/09/04") 
ending = rate.parse("2015/09/10")
b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
#   between 2015-09-03 and 2015-09-10 
#   => true
schedulable?訊息的傳送對象已從Bicycle轉變為Schedulable。現在你已經交給鴨子類型來處理 圖7.3則可以被調整成如圖7.4

實作Vehicle和Mechanic如何包括Schedulable模組 並且回應schedulable?訊息。
class Vehicle
	include Schedulable
	def lead_days 
		3
	end
	
	#...
end
class Mechanic 
	include Schedulable
	def lead_days
		4
	end
v = Vehicle.new
v.schedulable?(starting, ending)
# This Vehicle is not scheduled
#   between 2015-09-01 and 2015-09-10 
#   => true
m = Mechanic.new
m.schedulable?(starting, ending)
# This Mechanic is not scheduled
#   between 2015-08-31 and 2015-09-10 
#   => true
Schedulable裡的這段程式碼是抽象的,Schedulable覆蓋了lead_days,當schedulable?抵達任何 Schedulable時,該訊息會自動委派給這個模組中所定義的方法。
這個舉例可能並不符合嚴格的經典繼承定義,但撰寫技巧其實都是一樣的,因為都是沿著相同的路徑去尋找可 用方法。
今天又講了好大一篇,明天再接續瞭解是如何尋找相對應的方法,以及他的脈絡吧!
參考資料: